package com.vains.config.security;

import com.vains.constant.GlobalConstants;
import com.vains.constant.HttpStatusConstants;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;

/**
 * 重写Security自带的RemoteTokenServices中检查token的方法，使其能够返回一个统一的异常
 *
 * @author vains
 * @date 2021/4/5 0:48
 */
@Data
@Slf4j
@EqualsAndHashCode(callSuper = true)
public class CustomRemoteTokenServices extends RemoteTokenServices {

    private String clientId;

    private String clientSecret;

    private RestOperations restTemplate;

    private String checkTokenEndpointUrl;

    private static final String TOKEN_NAME = "token";

    private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();

    @Override
    public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
        if (StringUtils.isEmpty(accessToken)) {
            log.warn("access_token不能为空.");
            throw new InvalidTokenException("token异常：access_token不能为空.");
        }
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        formData.add(TOKEN_NAME, accessToken);
        HttpHeaders headers = new HttpHeaders();
        headers.set(GlobalConstants.HEADER_KEY_AUTHORIZATION, getAuthorizationHeader(clientId, clientSecret));
        Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);
        String errorKey = "error";
        if (map.containsKey(errorKey)) {
            log.warn("检查token端点返回错误：{}", map.get("error_description"));
            throw new InvalidTokenException("Token异常：" + map.get("error_description"));
        }
        String activeKey = "active";
        // gh-838
        if (!Boolean.TRUE.equals(map.get(activeKey))) {
            logger.debug("check_token returned active attribute: " + map.get(activeKey));
            throw new InvalidTokenException(accessToken);
        }

        return tokenConverter.extractAuthentication(map);
    }

    /**
     * 发送一个post请求
     * @param path 路径
     * @param formData 表单数据
     * @param headers 请求头
     * @return 返回Map 失败时会有一个key是error， 成功返回客户端信息
     */
    private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
        if (headers.getContentType() == null) {
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        }
        @SuppressWarnings("rawtypes")
        Map map = restTemplate.exchange(path, HttpMethod.POST,
                new HttpEntity<>(formData, headers), Map.class).getBody();
        @SuppressWarnings("unchecked")
        Map<String, Object> result = map;
        return result;
    }

    /**
     * 根据客户端id和秘钥获取一个Basic类型的请求头
     * @param clientId 客户端账号
     * @param clientSecret 客户端秘钥
     * @return 请求头
     */
    private String getAuthorizationHeader(String clientId, String clientSecret) {

        if(clientId == null || clientSecret == null) {
            logger.warn("Null Client ID or Client Secret detected. Endpoint that requires authentication will reject request with 401 error.");
        }

        String cred = String.format("%s:%s", clientId, clientSecret);
        return "Basic " + new String(Base64.getEncoder().encode(cred.getBytes(StandardCharsets.UTF_8)));
    }

    public CustomRemoteTokenServices() {
        // 实例化该类时实例化 restTemplate
        restTemplate = new RestTemplate();
        ((RestTemplate) restTemplate).setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            // Ignore 400
            public void handleError(ClientHttpResponse response) throws IOException {
                if (response.getRawStatusCode() != HttpStatusConstants.BAD_REQUEST) {
                    super.handleError(response);
                }
            }
        });
    }

}
